﻿namespace MvvmTools.NotifyPropertyChanged
{
    using System;
    using System.ComponentModel;
    using System.Diagnostics.Contracts;
    using System.Threading;
    using JetBrains.Annotations;

    internal sealed class ObservableNotificationListener<T, TElement> : ChangeListener<INotifyPropertyChanged>
        where T : INotifyPropertyChanged
    {
        private readonly Func<T, IObservable<TElement>> _observableGetter;
        private SubscriptionHolder _subscription;
        private int _changeId;

        public ObservableNotificationListener(
            [CanBeNull] ChangeListenerBase parent,
            [NotNull] Func<T, IObservable<TElement>> getter,
            [CanBeNull] string propertyName)
            : base(parent, o => null, propertyName)
        {
            Contract.Requires<ArgumentNullException>(getter != null);

            _observableGetter = getter;
            OpenSubscription();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                CloseSubscription();
            }

            base.Dispose(disposing);
        }

        protected override void Subscribe(bool applyToCurrent)
        {
            base.Subscribe(applyToCurrent);
            OpenSubscription();
        }

        protected override void Unsubscribe(bool applyToCurrent)
        {
            CloseSubscription();
            base.Unsubscribe(applyToCurrent);
        }

        private void OnSubscriptionNewData()
        {
            if (!IsSuspended)
            {
                RaiseChangedToRoot(this, new PropertyChange(Interlocked.Increment(ref _changeId), PropertyName));
            }
        }

        private void OpenSubscription()
        {
            var observableSource = _observableGetter((T)Target);
            if (observableSource != null)
            {
                _subscription = new SubscriptionHolder(this, observableSource);
            }
        }

        private void CloseSubscription()
        {
            if (_subscription != null)
            {
                _subscription.Dispose();
                _subscription = null;
            }
        }

        private sealed class SubscriptionHolder : IDisposable, IObserver<TElement>
        {
            private readonly WeakReference _ownerRef;
            private IDisposable _subscription;

            internal SubscriptionHolder(
                [NotNull] IPropertyObservable owner,
                [NotNull] IObservable<TElement> observableSource)
            {
                Contract.Requires<ArgumentNullException>(owner != null);
                Contract.Requires<ArgumentNullException>(observableSource != null);

                _ownerRef = new WeakReference(owner);
                _subscription = observableSource.Subscribe(this);
            }

            public void Dispose()
            {
                var token = Interlocked.Exchange(ref _subscription, null);
                if (token != null)
                {
                    token.Dispose();
                }
            }

            void IObserver<TElement>.OnNext(TElement value)
            {
                var listener = (ObservableNotificationListener<T, TElement>)_ownerRef.Target;
                if (listener != null)
                {
                    listener.OnSubscriptionNewData();
                }
                else
                {
                    Dispose();
                }
            }

            void IObserver<TElement>.OnError(Exception error)
            {
                Dispose();
            }

            void IObserver<TElement>.OnCompleted()
            {
                Dispose();
            }
        }
    }
}
